package com.epam.mycollections.maps;

import com.epam.mycollections.interfaces.MyMap;

import java.util.Iterator;

/**
 * My Personal implementation of HashMap data structure
 *
 * @version 1.0 Build 26.04.2014
 * @author Iurii Miedviediev
 */
public class MyHashMap<K,V> implements MyMap<K,V> {

    /**
     * SimpleEntry of MyHashMap
     * In this implementation method setValue return old value of the entry
     */
    public static class SimpleEntry<K,V> implements MyMap.Entry<K,V> {

        //Key->Value Pair
        private final K key;
        private V value;
        private SimpleEntry<K,V> next;

        //constructor with key and value
        public SimpleEntry(K key, V value) {
            this.key = key;
            this.value = value;
        }

        /**
         * Key Getter
         * @return Key of the entry
         */
        @Override
        public K getKey() {
            return this.key;
        }

        /**
         * Value Getter
         * @return Value of the Entry
         */
        @Override
        public V getValue() {
            return this.value;
        }

        /**
         * Set new Value for current Entry
         * @param value new desired value
         * @return old value of the entry
         */
        @Override
        public V setValue(V value) {
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        /**
         * Next entry getter
         * @return SimpleEntry with the same hash code, next to the current entry
         */
        public SimpleEntry<K,V> getNext() {
            return this.next;
        }

        /**
         * Next entry setter
         * @param newNext entry with the same hash code as current one
         */
        public void setNext(SimpleEntry<K,V> newNext) {
            this.next = newNext;
        }

        /**
         * Hash Code of the Entry (hash code of the key)
         * @return integer, result of hashing the key of the entry
         */
        @Override
        public int hashCode() {
            return this.key.hashCode();
        }

        /**
         * Check the equality with other Entry
         * @param other Entry to check
         * @return true, if current entry equals the other, false otherwise
         */
        @Override
        public boolean equals(Object other) {
            if(!(other instanceof SimpleEntry)) {
                return false;
            }
            SimpleEntry<K,V> otherEntry = (SimpleEntry) other;
            return this.key.equals(otherEntry.getKey()) && this.value.equals(otherEntry.getValue());
        }
    }

    private static final float DEFAULTLOADFACTOR = 0.75f;
    private static final int DEFAULTCAPACITY = 16;

    //capacity of the MyHashMap
    protected int capacity;

    //percentage of occupancy of the storage to enlarge its capacity when needed
    protected final float loadFactor;

    //array with all the entries of the MyHashMap
    protected SimpleEntry<K,V>[] storage;

    //number of Entries in the MyHashMap
    protected int length = 0;

    /* Constructors for MyHashMap */
    public MyHashMap(int initCapacity, float loadFactor) {
        if(initCapacity < 0 || loadFactor <= 0) {
            throw new IllegalArgumentException();
        }
        this.capacity = initCapacity;
        this.loadFactor = loadFactor;
        this.storage = new SimpleEntry[this.capacity];
    }

    public MyHashMap(int initCapacity) {
        this(initCapacity, DEFAULTLOADFACTOR);
    }

    public MyHashMap() {
        this(DEFAULTCAPACITY, DEFAULTLOADFACTOR);
    }
    /* End of Constructors */

    /**
     * Removes all of the mappings from this map
     */
    @Override
    public void clear() {
        this.storage = new SimpleEntry[this.capacity];
        this.length = 0;
    }

    /**
     * Returns true if this map contains a mapping for the specified key
     * @param key key to search for
     * @return true if key is in storage, false otherwise
     */
    @Override
    public boolean containsKey(K key) {
        return this.get(key) != null;
    }

    /**
     * Returns true if this map maps one or more keys to the specified value.
     * @param value value to search for
     * @return true if value is in the map, false otherwise
     */
    @Override
    public boolean containsValue(V value) {
        Iterator<Entry<K,V>> iter = this.entryIterator();
        while(iter.hasNext()) {
            if(iter.next().getValue().equals(value)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the value to which the specified key is mapped,
     * or null if this map contains no mapping for the key
     *
     * @param key key to find in the map
     * @return value of the key in the map or null if there is no such key
     */
    @Override
    public V get(K key) {
        int bucket = Math.abs(key.hashCode() % this.capacity);
        SimpleEntry<K,V> currentEntry = this.storage[bucket];
        while(currentEntry != null) {
            if(currentEntry.getKey().equals(key)) {
                return currentEntry.getValue();
            }
            currentEntry = currentEntry.getNext();
        }
        return null;
    }

    /**
     * Returns true if this map contains no key-value mappings
     * @return true, if length of the MyHashMap is zero, false otherwise
     */
    @Override
    public boolean isEmpty() {
        return this.length == 0;
    }

    /**
     * Associates the specified value with the specified key in this map.
     * Also ensures the capacity of the storage according to the load factor
     *
     * @param key Key of the Entry
     * @param value Value of the Entry
     * @return Object, newly created Entry
     */
    @Override
    public Entry<K,V> put(K key, V value) {
        SimpleEntry<K,V> newEntry = new SimpleEntry<>(key, value);

        //find start point (appropriate bucket)
        int bucket = Math.abs(key.hashCode() % this.capacity);
        SimpleEntry<K,V> currentEntry = this.storage[bucket];

        //if bucket is empty
        if(currentEntry == null) {
            this.storage[bucket] = newEntry;
        } else {
            SimpleEntry<K,V> prev = null;
            while(currentEntry != null) {
                //if there is an entry with equal key, change only value

                if (currentEntry.getKey().equals(key)) {
                    currentEntry.setValue(value);
                    return currentEntry;
                }
                prev = currentEntry;
                currentEntry = currentEntry.getNext();
            }
            //add new entry entry to the end of the bucket
            prev.setNext(newEntry);
        }
        this.length++;
        return newEntry;
    }

    /**
     * Removes the mapping for the specified key from this map if present.
     * @param key key to remove
     * @return removed Entry
     */
    @Override
    public Entry<K,V> remove(K key) {
        //find the start point
        int bucket = Math.abs(key.hashCode() % this.capacity);
        SimpleEntry<K,V> current = this.storage[bucket];

        //if there is no entry at all
        if(current == null) {
            return null;
        } else if(current.getKey().equals(key)) {
            this.storage[bucket] = current.getNext();
            this.length--;
            return current;
        }
        //find necessary entry in the chain
        while(current.getNext() != null) {
            if(current.getNext().getKey().equals(key)) {
                SimpleEntry<K,V> removed = current.getNext();
                current.setNext(current.getNext().getNext());
                this.length--;
                return removed;
            }
            current = current.getNext();
        }
        //if not found
        return null;
    }

    /**
     * Returns the number of key-value mappings in this map
     * @return integer, size of the MyHashMap
     */
    @Override
    public int size() {
        return this.length;
    }

    /**
     * Returns an iterator over the elements (MyMap.Entry) in proper sequence
     * @return self-made Iterator over Entries
     */
    @Override
    public Iterator<Entry<K,V>> entryIterator() {
        return new HashMapIterator();
    }

    /**
     * Implementation of the Iterator over hash map entries
     */
    private class HashMapIterator implements Iterator<Entry<K,V>> {

        SimpleEntry<K,V> currentEntry;
        int bucket = 0;
        int numEntry = length;

        @Override
        public boolean hasNext() {
            return numEntry > 0;
        }

        @Override
        public SimpleEntry<K,V> next() {
            while(bucket < capacity) {
                SimpleEntry<K,V> next = currentEntry;
                if(next != null) {
                    currentEntry = currentEntry.getNext();
                    numEntry--;
                    return next;
                } else {
                    currentEntry = storage[bucket++];
                }
            }
            return null;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}
